package org.apache.solr.schema;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.apache.lucene.analysis.util.ResourceLoader;
import org.apache.lucene.util.IOUtils;
import org.apache.noggit.JSONParser;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Exchange Rates Provider for {@link CurrencyField} implementing the freely available
 * exchange rates from openexchangerates.org
 * <p/>
 * <b>Disclaimer:</b> This data is collected from various providers and provided free of charge
 * for informational purposes only, with no guarantee whatsoever of accuracy, validity,
 * availability or fitness for any purpose; use at your own risk. Other than that - have
 * fun, and please share/watch/fork if you think data like this should be free!
 */
public class OpenExchangeRatesOrgProvider implements ExchangeRateProvider {

    public static Logger log = LoggerFactory.getLogger(OpenExchangeRatesOrgProvider.class);

    protected static final String PARAM_RATES_FILE_LOCATION = "ratesFileLocation";
    protected static final String PARAM_REFRESH_INTERVAL = "refreshInterval";
    protected static final String DEFAULT_RATES_FILE_LOCATION = "http://openexchangerates.org/latest.json";
    protected static final String DEFAULT_REFRESH_INTERVAL = "1440";
    protected String ratesFileLocation;
    protected int refreshInterval;
    protected ResourceLoader resourceLoader;
    protected OpenExchangeRates rates;

    /**
     * Returns the currently known exchange rate between two currencies. The
     * rates are fetched from the freely available OpenExchangeRates.org JSON,
     * hourly updated. All rates are symmetrical with base currency being USD by
     * default.
     *
     * @param sourceCurrencyCode The source currency being converted from.
     * @param targetCurrencyCode The target currency being converted to.
     * @return The exchange rate.
     * @throws SolrException if the requested currency pair cannot be found
     */
    @Override
    public double getExchangeRate(String sourceCurrencyCode, String targetCurrencyCode) {

        if (rates == null) {
            throw new SolrException(SolrException.ErrorCode.SERVICE_UNAVAILABLE, "Rates not initialized.");
        }

        if (sourceCurrencyCode == null || targetCurrencyCode == null) {
            throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Cannot get exchange rate; currency was null.");
        }

        if (rates.getTimestamp() + refreshInterval * 60 * 1000 > System.currentTimeMillis()) {
            log.debug("Refresh interval has expired. Refreshing exchange rates.");
            reload();
        }

        Double source = (Double) rates.getRates().get(sourceCurrencyCode);
        Double target = (Double) rates.getRates().get(targetCurrencyCode);

        if (source == null || target == null) {
            throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
                    "No available conversion rate from " + sourceCurrencyCode + " to " + targetCurrencyCode + ". "
                    + "Available rates are " + listAvailableCurrencies());
        }

        return target / source;
    }

    @Override
    public boolean equals(Object o) {

        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        OpenExchangeRatesOrgProvider that = (OpenExchangeRatesOrgProvider) o;

        return !(rates != null ? !rates.equals(that.rates) : that.rates != null);
    }

    @Override
    public int hashCode() {
        return rates != null ? rates.hashCode() : 0;
    }

    @Override
    public String toString() {
        return "[" + this.getClass().getName() + " : " + rates.getRates().size() + " rates.]";
    }

    @Override
    public Set<String> listAvailableCurrencies() {
        if (rates == null) {
            throw new SolrException(ErrorCode.SERVER_ERROR, "Rates not initialized");
        }
        return rates.getRates().keySet();
    }

    @Override
    public boolean reload() throws SolrException {

        InputStream ratesJsonStream = null;
        try {
            log.info("Reloading exchange rates from " + ratesFileLocation);
            try {
                ratesJsonStream = (new URL(ratesFileLocation)).openStream();
            }
            catch (Exception e) {
                ratesJsonStream = resourceLoader.openResource(ratesFileLocation);
            }

            rates = new OpenExchangeRates(ratesJsonStream);
            return true;
        }
        catch (Exception e) {
            throw new SolrException(ErrorCode.SERVER_ERROR, "Error reloading exchange rates", e);
        }
        finally {
            if (ratesJsonStream != null) {
                try {
                    ratesJsonStream.close();
                }
                catch (IOException e) {
                    throw new SolrException(ErrorCode.SERVER_ERROR, "Error closing stream", e);
                }
            }
        }
    }

    @Override
    public void init(Map<String, String> params) throws SolrException {

        try {
            ratesFileLocation = getParam(params.get(PARAM_RATES_FILE_LOCATION), DEFAULT_RATES_FILE_LOCATION);
            refreshInterval = Integer.parseInt(getParam(params.get(PARAM_REFRESH_INTERVAL), DEFAULT_REFRESH_INTERVAL));
            // Force a refresh interval of minimum one hour, since the API does not offer better resolution
            if (refreshInterval < 60) {
                refreshInterval = 60;
                log.warn("Specified refreshInterval was too small. Setting to 60 minutes which is the update rate of openexchangerates.org");
            }
            log.info("Initialized with rates=" + ratesFileLocation + ", refreshInterval=" + refreshInterval + ".");
        }
        catch (Exception e) {
            throw new SolrException(ErrorCode.BAD_REQUEST, "Error initializing", e);
        } 
        finally {
            // Removing config params custom to us
            params.remove(PARAM_RATES_FILE_LOCATION);
            params.remove(PARAM_REFRESH_INTERVAL);
        }
    }

    @Override
    public void inform(ResourceLoader loader) throws SolrException {
        resourceLoader = loader;
        reload();
    }

    private String getParam(String param, String defaultParam) {
        return param == null ? defaultParam : param;
    }

    /**
     * A simple class encapsulating the JSON data from openexchangerates.org
     */
    class OpenExchangeRates {

        private Map<String, Double> rates;
        private String baseCurrency;
        private long timestamp;
        private String disclaimer;
        private String license;
        private JSONParser parser;

        public OpenExchangeRates(InputStream ratesStream) throws IOException {
            parser = new JSONParser(new InputStreamReader(ratesStream, IOUtils.CHARSET_UTF_8));
            rates = new HashMap<>();

            int ev;
            do {
                ev = parser.nextEvent();
                switch (ev) {
                    case JSONParser.STRING:
                        if (parser.wasKey()) {
                            String key = parser.getString();
                            switch (key) {
                                case "disclaimer":
                                    parser.nextEvent();
                                    disclaimer = parser.getString();
                                    break;
                                case "license":
                                    parser.nextEvent();
                                    license = parser.getString();
                                    break;
                                case "timestamp":
                                    parser.nextEvent();
                                    timestamp = parser.getLong();
                                    break;
                                case "base":
                                    parser.nextEvent();
                                    baseCurrency = parser.getString();
                                    break;
                                case "rates":
                                    ev = parser.nextEvent();
                                    assert (ev == JSONParser.OBJECT_START);
                                    ev = parser.nextEvent();
                                    while (ev != JSONParser.OBJECT_END) {
                                        String curr = parser.getString();
                                        ev = parser.nextEvent();
                                        Double rate = parser.getDouble();
                                        rates.put(curr, rate);
                                        ev = parser.nextEvent();
                                    }
                                    break;
                                default:
                                    log.warn("Unknown key " + key);
                                    break;
                            }
                            break;
                        }
                        else {
                            log.warn("Expected key, got " + JSONParser.getEventString(ev));
                            break;
                        }

                    case JSONParser.OBJECT_END:
                    case JSONParser.OBJECT_START:
                    case JSONParser.EOF:
                        break;

                    default:
                        log.info("Noggit UNKNOWN_EVENT_ID:" + JSONParser.getEventString(ev));
                        break;
                }
            } while (ev != JSONParser.EOF);
        }

        public Map<String, Double> getRates() {
            return rates;
        }

        public long getTimestamp() {
            return timestamp;
        }

        public String getDisclaimer() {
            return disclaimer;
        }

        public String getBaseCurrency() {
            return baseCurrency;
        }

        public String getLicense() {
            return license;
        }
    }
}
